package skyproc;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.zip.DataFormatException;
import lev.*;
import skyproc.MajorRecord.MajorFlags;
import skyproc.SPGlobal.Language;
import skyproc.SubStringPointer.Files;
import skyproc.exceptions.BadMod;
import skyproc.exceptions.MissingMaster;
import skyproc.gui.SPProgressBarPlug;

/**
 * Used to import data from files into Mod objects that are ready to be
 * manipulated/exported.
 *
 * @author Justin Swanson
 */
public class SPImporter {

    private static String header = "Importer";
    private static String debugPath = "Mod Import/";

    /**
     * A placeholder constructor not meant to be called.<br> An SPImporter
     * object should only be instantiated as an extended class that overrides
     * the importControl() function.
     */
    public SPImporter() {
    }

    /**
     * Creates a new thread and runs any code input importControl input the
     * background. This is useful for GUI programs where you don't want the
     * program to freeze while it processes. <br> <br> NOTE: You MUST override
     * importControl() with custom code telling the program what you want it to
     * do input the background.
     */
    static public void runBackgroundImport() {
        (new Thread(new StartImportThread())).start();
    }

    private static class StartImportThread implements Runnable {

        @Override
        public void run() {
        }

        public void main(String args[]) {
            (new Thread(new StartImportThread())).start();
        }
    }

    /**
     * Loads input plugins.txt and reads input the mods the user has active and
     * returns an ArrayList of the ModListings input load order. If a mod being
     * read input fits the criteria of isModToSkip(), then it will be omitted
     * input the results.<br> If any mods on the list are not present input the
     * data folder, they will be skipped.<br><br> If the program cannot locate
     * plugins.txt, it will prompt the user to locate the file themselves. Once
     * they do, it will create a file and use that as a reference for future
     * patch generations.<br><br> Related settings input SPGlobal:<br> -
     * pluginsListPath<br> - pathToData<br> - pluginListBackupPath
     *
     * @see SPGlobal
     * @return An ArrayList of ModListings of all active mods present input the
     * data folder.
     * @throws java.io.IOException
     */
    static public ArrayList<ModListing> getActiveModList() throws java.io.IOException {
        if (SPDatabase.activePlugins.isEmpty()) {
            SPGlobal.sync(true);
            SPGlobal.newSyncLog("Get Active Mod List.txt");
            String header = "IMPORT MODS";
            BufferedReader ModFile;
            String dataFolder = SPGlobal.getPluginsTxt();

            //Open Plugin file
            ModFile = new BufferedReader(new FileReader(dataFolder));

            try {
                String line = ModFile.readLine();
                ArrayList<String> lines = new ArrayList<>();
                SPGlobal.logSync(header, "Loading in Active Plugins");
                File pluginName;

                //If Skyrim, add Skyrim.esm and Update.esm, as they
                //are automatically removed from the list, and assumed
                if (SPGlobal.gameName.equals("Skyrim")) {
                    lines.add("Skyrim.esm");
                    lines.add("Update.esm");
                }

                while (line != null) {
                    if (line.indexOf("#") >= 0) {
                        line = line.substring(0, line.indexOf("#"));
                    }
                    line = line.trim();
                    if (!line.equals("")) {
                        pluginName = new File(SPGlobal.pathToData + line);
                        ModListing nextMod = new ModListing(line);
                        if (SPGlobal.noModsAfter && nextMod.equals(SPGlobal.getGlobalPatch().getInfo())) {
                            SPGlobal.logSync(header, "Skipping the remaining mods as they were after the patch.");
                            break;
                        } else if (SPGlobal.shouldImport(nextMod)
                                && SPGlobal.shouldImport(line)) {
                            if (pluginName.isFile()) {
                                if (Ln.indexOfIgnoreCase(lines, line) == -1) {
                                    SPGlobal.logSync(header, "Adding mod: " + line);
                                    lines.add(line);
                                } else if (SPGlobal.logging()) {
                                    SPGlobal.logSync(header, "Mod was already added: ", line);
                                }
                            } else if (SPGlobal.logging()) {
                                SPGlobal.logSync(header, "Mod didn't exist: ", line);
                            }
                        } else if (SPGlobal.logging()) {
                            SPGlobal.logSync(header, "Mod was on the list to skip: " + line);
                        }
                    }
                    line = ModFile.readLine();
                }

                SPGlobal.sync(false);
                SPDatabase.activePlugins = sortModListings(lines);

            } catch (java.io.FileNotFoundException e) {
                SPGlobal.logException(e);
                SPGlobal.sync(false);
                throw e;
            } catch (java.io.IOException e) {
                SPGlobal.logException(e);
                SPGlobal.sync(false);
                throw e;
            }
        }
        return SPDatabase.activePlugins;
    }

    /**
     * Will scan the data folder and ModListings for all .esp and .esm files,
     * regardless of if they are active. It will return a complete list input
     * load order. If a mod being read input fits the criteria of isModToSkip(),
     * then it will be omitted input the results.<br><br> Related settings input
     * SPGlobal:<br> - pathToData
     *
     * @see SPGlobal
     * @return ArrayList of ModListings of all mods present input the data
     * folder.
     */
    static public ArrayList<ModListing> getModList() {
        SPGlobal.newSyncLog("Get All Present Mod List.txt");
        File directory = new File(SPGlobal.pathToData);
        ArrayList<String> out = new ArrayList<>();
        if (directory.isDirectory()) {
            File[] files = directory.listFiles();
            for (File f : files) {
                String name = f.getName().toUpperCase();
                if (name.contains(".ESP") || name.contains(".ESM")) {
                    if (!SPGlobal.modsToSkip.contains(new ModListing(f.getName()))) {
                        out.add(f.getName());
                    } else if (SPGlobal.logging()) {
                        SPGlobal.logSync(header, "Mod was on the list to skip: " + name);
                    }
                }
            }
        }
        return sortModListings(out);
    }

    static ArrayList<ModListing> sortModListings(ArrayList<String> lines) {
        SPGlobal.sync(true);
        //Read it input
        ArrayList<String> esms = new ArrayList<>();
        ArrayList<String> esps = new ArrayList<>();

        for (String line : lines) {
            try {
                RecordFileChannel input = new RecordFileChannel(SPGlobal.pathToData + line);
                ModListing tempListing = new ModListing(line);
                Mod plugin = new Mod(tempListing, extractHeaderInfo(input));
                if (plugin.isFlag(Mod.Mod_Flags.MASTER)) {
                    esms.add(line);
                } else {
                    esps.add(line);
                }
            } catch (Exception e) {
                if (SPGlobal.logging()) {
                    SPGlobal.logSync(header, "Could not read mod file: " + lines + "\nSorting based on extension.");
                }
                if (line.toUpperCase().contains(".ESM")) {
                    esms.add(line);
                } else {
                    esps.add(line);
                }
            }

        }

        SPGlobal.flush();

        ArrayList<ModListing> listing = new ArrayList<>();
        for (String m : esms) {
            listing.add(new ModListing(m));
            //false master check
        }
        for (String m : esps) {
            listing.add(new ModListing(m));
        }

        if (SPGlobal.logging()) {
            SPGlobal.logSync(header, "=========  Final sorted load order : ==========");
            int counter = 0;
            for (ModListing m : listing) {
                SPGlobal.logSync(header, Ln.prettyPrintHex(counter++) + " Name: " + m.print());
            }
        }

        SPGlobal.sync(false);
        return listing;
    }

    /**
     * Imports all mods input the user's Data/ folder, no matter if they are
     * currently active or not. Imports all GRUPs currently supported by
     * SkyProc. If a mod being read input fits the criteria of isModToSkip(),
     * then it will be omitted input the results.<br><br> NOTE: It is suggested
     * for speed reasons to only import the GRUP types you are interested input
     * by using other import functions.
     *
     * @return A set of Mods with all their data imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importAllMods() throws MissingMaster {
        return importAllMods(GRUP_TYPE.values());
    }

    /**
     * Imports all mods input the user's Data/ folder, no matter if they are
     * currently active or not. Imports only GRUPS specified input the
     * parameter. If a mod being read input fits the criteria of isModToSkip(),
     * then it will be omitted input the results.<br><br> Related settings input
     * SPGlobal:<br> - pathToData
     *
     * @see SPGlobal
     * @param grup_targets Any amount of GRUP targets, separated by commas, that
     * you wish to import.
     * @return A set of Mods with specified GRUPs imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importAllMods(GRUP_TYPE... grup_targets) throws MissingMaster {
        return importMods(getModList(), SPGlobal.pathToData, grup_targets);
    }

    /**
     * Imports all mods input the user's Data/ folder, no matter if they are
     * currently active or not. Imports only GRUPS specified input the
     * parameter. If a mod being read input fits the criteria of isModToSkip(),
     * then it will be omitted input the results.<br><br> Related settings input
     * SPGlobal:<br> - pathToData
     *
     * @see SPGlobal
     * @param grup_targets An arraylist of GRUP_TYPE with the desired types to
     * import
     * @return A set of Mods with specified GRUPs imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importAllMods(ArrayList<GRUP_TYPE> grup_targets) throws MissingMaster {
        GRUP_TYPE[] tmp = new GRUP_TYPE[0];
        return importMods(getModList(), SPGlobal.pathToData, grup_targets.toArray(tmp));
    }

    /**
     * Loads input plugins.txt and reads input the mods the user has active, and
     * loads only those that are also present input the data folder. Imports all
     * GRUPs currently supported by SkyProc. If a mod being read input fits the
     * criteria of isModToSkip(), then it will be omitted input the
     * results.<br><br> If the program cannot locate plugins.txt, it will prompt
     * the user to locate the file themselves. Once they do, it will create a
     * file and use that as a reference for future patch generations.<br><br>
     * NOTE: It is suggested for speed reasons to only import the GRUP types you
     * are interested input by using other import functions.<br><br> Related
     * settings input SPGlobal:<br> - pluginsListPath<br> - pathToData<br> -
     * pluginListBackupPath
     *
     * @see SPGlobal
     * @return A set of Mods with all their data imported and ready to be
     * manipulated.
     * @throws IOException
     * @throws MissingMaster
     */
    static public Set<Mod> importActiveMods() throws IOException, MissingMaster {
        return importActiveMods(GRUP_TYPE.values());
    }

    /**
     * Loads input plugins.txt and reads input the mods the user has active, and
     * loads only those that are also present input the data folder. Imports
     * only GRUPS specified input the parameter. If a mod being read input fits
     * the criteria of isModToSkip(), then it will be omitted input the
     * results.<br><br> If the program cannot locate plugins.txt, it will prompt
     * the user to locate the file themselves. Once they do, it will create a
     * file and use that as a reference for future patch generations.<br><br>
     * Related settings input SPGlobal:<br> - pluginsListPath<br> -
     * pathToData<br> - pluginListBackupPath
     *
     * @see SPGlobal
     * @param grup_targets Any amount of GRUP targets, separated by commas, that
     * you wish to import.
     * @return A set of Mods with specified GRUPs imported and ready to be
     * manipulated.
     * @throws IOException
     * @throws MissingMaster
     */
    static public Set<Mod> importActiveMods(GRUP_TYPE... grup_targets) throws IOException, MissingMaster {
        return importMods(getActiveModList(), SPGlobal.pathToData, grup_targets);
    }

    /**
     * Loads input plugins.txt and reads input the mods the user has active, and
     * loads only those that are also present input the data folder. Imports
     * only GRUPS specified input the parameter. If a mod being read input fits
     * the criteria of isModToSkip(), then it will be omitted input the
     * results.<br><br> If the program cannot locate plugins.txt, it will prompt
     * the user to locate the file themselves. Once they do, it will create a
     * file and use that as a reference for future patch generations.<br><br>
     * Related settings input SPGlobal:<br> - pluginsListPath<br> -
     * pathToData<br> - pluginListBackupPath
     *
     * @see SPGlobal
     * @param grup_targets An arraylist of GRUP_TYPE with the desired types to
     * import
     * @return A set of Mods with specified GRUPs imported and ready to be
     * manipulated.
     * @throws IOException
     * @throws MissingMaster
     */
    static public Set<Mod> importActiveMods(ArrayList<GRUP_TYPE> grup_targets) throws IOException, MissingMaster {
        GRUP_TYPE[] tmp = new GRUP_TYPE[0];
        return importMods(getActiveModList(), SPGlobal.pathToData, grup_targets.toArray(tmp));
    }

    /**
     * Looks for mods that match the given ModListings inside the data folder.
     * It imports any that are properly located, and loads input only GRUPS
     * specified input the parameter.<br><br> Related settings input
     * SPGlobal:<br> - pathToData<br>
     *
     * @see SPGlobal
     * @param mods ModListings to look for and import from the data folder.
     * @param grup_targets Any amount of GRUP targets, separated by commas, that
     * you wish to import.
     * @return A set of Mods with specified GRUPs imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importMods(ArrayList<ModListing> mods, GRUP_TYPE... grup_targets) throws MissingMaster {
        return importMods(mods, SPGlobal.pathToData, grup_targets);
    }

    /**
     * Looks for mods that match the given ModListings inside the data folder.
     * It imports any that are properly located, and loads input only GRUPS
     * specified input the parameter.<br><br> Related settings input
     * SPGlobal:<br> - pathToData<br>
     *
     * @see SPGlobal
     * @param mods ModListings to look for and import from the data folder.
     * @param grup_targets An arraylist of GRUP_TYPE with the desired types to
     * import
     * @return A set of Mods with specified GRUPs imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importMods(ArrayList<ModListing> mods, ArrayList<GRUP_TYPE> grup_targets) throws MissingMaster {
        GRUP_TYPE[] tmp = new GRUP_TYPE[0];
        return importMods(mods, SPGlobal.pathToData, grup_targets.toArray(tmp));
    }

    /**
     * Looks for mods that match the given ModListings inside the data folder.
     * It imports any that are properly located, and loads ALL GRUPs supported
     * by SkyProc.<br><br> NOTE: It is suggested for speed reasons to only
     * import the GRUP types you are interested input by using other import
     * functions.<br><br> Related settings input SPGlobal:<br> - pathToData
     *
     * @see SPGlobal
     * @param mods ModListings to look for and import from the data folder.
     * @return A set of Mods with all GRUPs imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importMods(ArrayList<ModListing> mods) throws MissingMaster {
        return importMods(mods, SPGlobal.pathToData, GRUP_TYPE.values());
    }

    /**
     * Looks for mods that match the given ModListings input the path specified.
     * It imports any that are properly located, and loads ALL GRUPs supported
     * by SkyProc.<br><br> NOTE: It is suggested for speed reasons to only
     * import the GRUP types you are interested input by using other import
     * functions.
     *
     * @param mods ModListings to look for and import from the data folder.
     * @param path Path from patch location to where to load mods from.
     * @return A set of Mods with all GRUPs imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importMods(ArrayList<ModListing> mods, String path) throws MissingMaster {
        return importMods(mods, path, GRUP_TYPE.values());
    }

    /**
     * Looks for mods that match the given ModListings input the path specified.
     * It imports any that are properly located, and loads input only GRUPS
     * specified input the parameter.
     *
     * @param mods ModListings to look for and import from the data folder.
     * @param path Path from patch location to where to load mods from.
     * @param grup_targets Any amount of GRUP targets, separated by commas, that
     * you wish to import.
     * @return A set of Mods with specified GRUPs imported and ready to be
     * manipulated.
     * @throws MissingMaster
     */
    static public Set<Mod> importMods(final ArrayList<ModListing> mods, String path, GRUP_TYPE... grup_targets) throws MissingMaster {

        if (grup_targets.length == 0) {
            SPGlobal.logMain(header, "Skipping import because requests were empty.");
            return new HashSet<>(0);
        }

        if (SPGlobal.logging()) {
            SPGlobal.logMain(header, "Starting import of targets: ");
            String grups = "";
            for (GRUP_TYPE g : grup_targets) {
                grups += "   " + g.toString() + " ";
            }
            SPGlobal.logMain(header, grups);
            SPGlobal.logMain(header, "In mods: ");
            for (ModListing m : mods) {
                SPGlobal.logMain(header, "   " + m.print());
            }

        }
        String header = "Import Mods";

        Set<Mod> outSet = new TreeSet<>();

        SPProgressBarPlug.setMax(mods.size(), "Importing plugins.");

        for (int i = 0; i < mods.size(); i++) {
            String mod = mods.get(i).print();
            SPProgressBarPlug.setStatusNumbered(genStatus(mods.get(i)));
            if (!SPGlobal.modsToSkip.contains(new ModListing(mod))) {
                try {
                    outSet.add(importMod(new ModListing(mod), i, path, true, grup_targets));
                } catch (MissingMaster m) {
                    throw m;
                } catch (BadMod ex) {
                    SPGlobal.logError(header, "Skipping a bad mod: " + mod);
                    SPGlobal.logError(header, "  " + ex.toString());
                } catch (Exception e) {
                    SPGlobal.logError(header, "Exception occured while importing mod : " + mod);
                    SPGlobal.logError(header, "  Message: " + e);
                    SPGlobal.logError(header, "  Stack: ");
                    for (StackTraceElement s : e.getStackTrace()) {
                        SPGlobal.logError(header, "  " + s.toString());
                    }
                }
            } else {
                SPProgressBarPlug.setStatusNumbered(genStatus(mods.get(i)) + ": Skipped!");
            }
            SPProgressBarPlug.incrementBar();
        }

        if (SPGlobal.logging()) {
            SPGlobal.logMain(header, "Done Importing Mods.");
        }
        return outSet;
    }

    /**
     * Looks for a mod matching the ModListing inside the given path. If
     * properly located, it imports only GRUPS specified input the parameter.
     *
     * @param listing Mod name and suffix to look for.
     * @param path Path to look for the mod data.
     * @param grup_targets Any amount of GRUP targets, separated by commas, that
     * you wish to import.
     * @return A mod with the specified GRUPs imported and ready to be
     * manipulated.
     * @throws BadMod If SkyProc runs into any unexpected data structures, or
     * has any error importing a mod at all.
     * @throws MissingMaster
     */
    static public Mod importMod(ModListing listing, String path, GRUP_TYPE... grup_targets) throws BadMod, MissingMaster {
        return importMod(listing, 1000, path, true, grup_targets);
    }

    /**
     * Looks for a mod matching the ModListing inside the given path. If
     * properly located, it imports only GRUPS specified input the parameter.
     *
     * @param listing Mod name and suffix to look for.
     * @param path Path to look for the mod data.
     * @param grup_targets An ArrayList of GRUP targets that you wish to import.
     * @return A mod with the specified GRUPs imported and ready to be
     * manipulated.
     * @throws BadMod If SkyProc runs into any unexpected data structures, or
     * has any error importing a mod at all.
     *
     * public Mod importMod(ModListing listing, String path,
     * ArrayList<GRUP_TYPE> grup_targets) throws BadMod { GRUP_static Type[]
     * types = new GRUP_TYPE[grup_targets.size()]; types =
     * grup_targets.toArray(types); return importMod(listing, path, types);
     */
    static Mod importMod(ModListing listing, int index, String path, Boolean addtoDb, GRUP_TYPE... grup_targets) throws BadMod, MissingMaster {
        if (!Consistency.isImported()) {
            Consistency.importConsistency(true);
        }
        SPGlobal.sync(true);
        try {
            RecordFileChannel input = new RecordFileChannel(path + listing.print());
            Mod plugin = new Mod(listing, extractHeaderInfo(input));
            SPGlobal.logMod(plugin, header, "Opened filestream to mod: " + listing.print());
            if (SPGlobal.checkMissingMasters) {
                checkMissingMasters(plugin);
            }
            plugin.input = input;
            if (SPGlobal.streamMode) {
                plugin.input = input;
            }

            if (plugin.isFlag(Mod.Mod_Flags.STRING_TABLED)) {
                importStringLocations(plugin);
                plugin.openStringStreams();
            }

            GRUPIterator iter = new GRUPIterator(grup_targets, input);
            while (iter.hasNext()) {
                String result = iter.loading();
                SPProgressBarPlug.setStatusNumbered(genStatus(listing) + ": " + result);
                SPGlobal.logMod(plugin, header, "================== Loading in GRUP " + result + ": ", plugin.getName(), " ===================");
                plugin.parseData(result, iter.next());
                SPGlobal.flush();
            }

            if (addtoDb) {
                SPGlobal.getDB().add(plugin);
            }

            if (!SPGlobal.streamMode) {
                input.close();
            }
            SPProgressBarPlug.setStatusNumbered(genStatus(listing) + ": Done");

            return plugin;
        } catch (MissingMaster m) {
            throw m;
        } catch (Exception e) {
            SPGlobal.logException(e);
            throw new BadMod("Ran into an exception, check SPGlobal.logs for more details.");
        } finally {
            SPGlobal.sync(false);
        }
    }

    /**
     * Looks for a mod matching the ModListing inside the given path. If
     * properly located, it imports only GRUPS specified input the parameter.
     *
     * @param listing Mod name and suffix to look for.
     * @param grup_targets An ArrayList of GRUP targets that you wish to import.
     * @return A mod with the specified GRUPs imported and ready to be
     * manipulated.
     * @throws BadMod If SkyProc runs into any unexpected data structures, or
     * has any error importing a mod at all.
     *
     * public Mod importMod(ModListing listing, String path,
     * ArrayList<GRUP_TYPE> grup_targets) throws BadMod { GRUP_static Type[]
     * types = new GRUP_TYPE[grup_targets.size()]; types =
     * grup_targets.toArray(types); return importMod(listing, path, types);
     * @throws MissingMaster
     */
    public static Mod importMod(ModListing listing, GRUP_TYPE... grup_targets) throws BadMod, MissingMaster {
        return importMod(listing, SPGlobal.pathToData, grup_targets);
    }

    static void checkMissingMasters(Mod plugin) throws MissingMaster {
        ArrayList<ModListing> missingMasters = new ArrayList<>();
        for (ModListing master : plugin.getMasters()) {
            try {
                RecordFileChannel input = new RecordFileChannel(SPGlobal.pathToData + master.print());
                ModListing tempListing = new ModListing(master.print());
                Mod masterMod = new Mod(tempListing, extractHeaderInfo(input));
                if (SPGlobal.getDB().getMod(tempListing) == null && SPGlobal.shouldImport(master)) {
                    missingMasters.add(master);
                }
            } catch (Exception exception) {
                missingMasters.add(master);
            }
        }
        if (!missingMasters.isEmpty()) {
            String error = "\n" + plugin.toString() + " has some missing masters:";
            for (ModListing m : missingMasters) {
                error += "\n  - " + m.toString();
            }
            throw new MissingMaster(error);
        }
    }

    /**
     * A rudimentary mod data Iterator that returns data of subrecords matching
     * typestring. Will parse all active mods. NOTE: Not for general use and not
     * heavily tested. Use at your own risk.
     *
     * @param typeString
     * @param grups
     * @return
     */
    static public DirtyParsingIterator getSubRecordsInGRUPs(String typeString, String... grups) {
        return new DirtyParsingIterator(typeString, grups);
    }

    /**
     *
     * A rudimentary mod data Iterator that returns data of subrecords matching
     * typestring. NOTE: Not for general use and not heavily tested. Use at your
     * own risk.
     *
     * @param targetMod
     * @param typeString
     * @param grups
     * @return
     */
    static public DirtyParsingIterator getSubRecordsInGRUPs(ModListing targetMod, String typeString, String... grups) {
        return new DirtyParsingIterator(targetMod, typeString, grups);
    }

    static class GRUPIterator implements Iterator<RecordShrinkArray> {

        LInChannel input;
        ArrayList<String> targets;
        String loading;

        GRUPIterator() {
            targets = new ArrayList<>(0);
        }

        GRUPIterator(GRUP_TYPE[] grup_targets, LInChannel input) {
            ArrayList<GRUP_TYPE> tmp = new ArrayList<>(Arrays.asList(grup_targets));
            for (GRUP_TYPE g : new ArrayList<>(tmp)) {
                if (GRUP_TYPE.unfinished(g) && !GRUP_TYPE.internal(g)) {
                    tmp.remove(g);
                }
            }
            targets = new ArrayList<>(tmp.size());
            for (GRUP_TYPE g : tmp) {
                targets.add(g.toString());
            }
            this.input = input;
        }

        @Override
        public boolean hasNext() {
            if (targets.isEmpty()) {
                return false;
            }
            try {
                loading = scanToGRUPStart(input, targets);
                targets.remove(loading);
                return !"NULL".equals(loading);
            } catch (IOException ex) {
                SPGlobal.logException(ex);
                return false;
            }
        }

        @Override
        public RecordShrinkArray next() {
            RecordShrinkArray out;
            try {
                out = extractGRUPData(input);
            } catch (IOException ex) {
                SPGlobal.logException(ex);
                out = new RecordShrinkArray();
            }
            return out;
        }

        public String loading() {
            return loading;
        }

        @Override
        public void remove() {
        }
    }

    /**
     * A rudimentary parser/iterator that returns data of subrecords with the
     * desired typestring.
     */
    public static class DirtyParsingIterator implements Iterator<RecordShrinkArray> {

        String typeString;
        ArrayList<String> grups;
        LImport input;
        LInChannel fileInput;
        LShrinkArray uncompressed = new LShrinkArray(new byte[0]);
        String inputStr = "";
        String majorRecordType = "";
        RecordShrinkArray next;
        ArrayList<ModListing> activeMods;
        ModListing activeMod;

        DirtyParsingIterator(String typeString, String[] grups) {
            init(typeString, grups);
            try {
                activeMods = SPImporter.getActiveModList();
            } catch (IOException ex) {
                activeMods = new ArrayList<>();
                SPGlobal.logException(ex);
            }
            switchToNextMod();
        }

        DirtyParsingIterator(ModListing mod, String typeString, String[] grups) {
            init(typeString, grups);
            activeMods = new ArrayList<>();
            activeMods.add(mod);
            switchToNextMod();
        }

        void init(String typeString, String[] grupTypes) {
            this.typeString = typeString;
            grups = new ArrayList<>(Arrays.asList(grupTypes));
        }

        /**
         *
         * @return Current mod being imported from.
         */
        public ModListing activeMod() {
            return activeMod;
        }

        /**
         *
         * @return
         */
        @Override
        public boolean hasNext() {
            grabNext();
            return next != null;
        }

        /**
         *
         * @return
         */
        @Override
        public RecordShrinkArray next() {
            return next;
        }

        void grabNext() {

            while (fileInput.available() >= 4 || uncompressed.available() >= 4) {
                if (uncompressed.available() >= 4) {
                    input = uncompressed;
                } else {
                    input = fileInput;
                }
                inputStr = input.extractString(0, 4);
                if ("TES4".equals(inputStr)) {
                    input.skip(input.extractInt(4) + 16);
                } else if ("GRUP".equals(inputStr)) {
                    int size = input.extractInt(4);
                    String tmpMajorType = input.extractString(0, 4);
                    if (grups.contains(tmpMajorType)) {
                        input.skip(12);
                        majorRecordType = tmpMajorType;
                    } else if (!tmpMajorType.matches("[a-zA-Z0-9_]*")) {
                        input.skip(12);
                    } else {
                        input.skip(size - 12);
                    }
//		    System.out.println("Passed GRUP");
                } else if (majorRecordType.equals(inputStr)
                        || "REFR".equals(inputStr)
                        || "ACHR".equals(inputStr)
                        || "LAND".equals(inputStr)
                        || "NAVM".equals(inputStr)
                        || "NVNM".equals(inputStr)) {
                    // If major record is target type
                    if (typeString.equals(inputStr)) {
                        next = new RecordShrinkArray(input.extract(input.extractInt(4) + 16));
                        return;
                    }

                    // Uncompress if compressed
                    int size = input.extractInt(4);
                    LFlags flags = new LFlags(input.extract(4));
                    input.extract(4); //FormID
                    input.skip(8);

                    if (inputStr.equals("NAVM")) {
                        input.skip(size);
//			System.out.println("Skipping size:  " + size);
                        continue;
                    }

                    if (flags.get(MajorFlags.Compressed.value)) {
                        uncompressed = new LShrinkArray(input.extract(size));
                        try {
                            uncompressed = uncompressed.correctForCompression();
                        } catch (DataFormatException ex) {
                            SPGlobal.logException(ex);
                        }
                    }

                } else if (typeString.equals(inputStr)) {
                    // Found target type
                    next = new RecordShrinkArray(input.extract(input.extractInt(2)));
                    return;
                } else {
                    // Skip Subrecord
//		    System.out.println(inputStr + " sub record");
                    input.skip(input.extractInt(2));
                }
            }

            if (switchToNextMod()) {
                grabNext();
            }
        }

        boolean switchToNextMod() {
            if (fileInput != null) {
                fileInput.close();
            }
            // If we have mods left, switch to it.
            if (activeMods.size() > 0) {
                fileInput = new LInChannel(SPGlobal.pathToData + activeMods.get(0).print());
                activeMod = activeMods.get(0);
                activeMods.remove(0);
                return true;
            } else {
                next = null;
            }
            return false;
        }

        /**
         *
         */
        @Override
        public void remove() {
        }
    }

    static ByteBuffer extractHeaderInfo(LInChannel in) {
        if (Ln.arrayToString(in.extractInts(0, 4)).equals("TES4")) {
            int size = Ln.arrayToInt(in.extractInts(0, 4)) + 24;  // +24 for TES4 extra info
            in.skip(-8); // To start of TES4 header
            return in.extractByteBuffer(0, size);
        }
        return ByteBuffer.allocate(0);
    }

    static void importStringLocations(Mod mod) {
        String header = "Importing Strings";
        if (SPGlobal.logging()) {
            SPGlobal.logMod(mod, header, "Importing Strings");
        }
        for (Files f : SubStringPointer.Files.values()) {
            try {
                importStringLocations(mod, f);
            } catch (Exception e) {
                SPGlobal.logError(header, "Error Importing Strings " + f + ": " + e);
            }
        }
    }

    static void importStringLocations(Mod plugin, SubStringPointer.Files file) throws FileNotFoundException, IOException, DataFormatException {

        ArrayList<Language> languageList = new ArrayList<>();
        languageList.add(SPGlobal.language);
        languageList.addAll(Arrays.asList(Language.values()));
        LShrinkArray in = null;
        int numRecords = 0;
        int recordsSize = 0;

        for (Language l : languageList) {
            String strings = getStringFilePath(plugin, l, file);
            File stringsFile = new File(SPGlobal.pathToData + strings);

            // Open file
            if (stringsFile.isFile()) {
                LInChannel istream = new LInChannel(stringsFile);
                // Read header
                numRecords = istream.extractInt(0, 4);
                recordsSize = numRecords * 8 + 8;
                in = new LShrinkArray(istream.extractByteBuffer(4, recordsSize));
            } else if (BSA.hasBSA(plugin)) {
                //In BSA
                BSA bsa = BSA.getBSA(plugin);
                bsa.loadFolders();
                if (!bsa.hasFile(strings)) {
                    continue;
                }
                in = bsa.getFile(strings);
                numRecords = in.extractInt(4);
                recordsSize = numRecords * 8 + 8;
                //Skip bytes 4-8
                in.skip(4);
            }

            // Found strings, read entry pairs
            if (in != null) {
                plugin.language = l;
                for (int i = 0; i < numRecords; i++) {
                    plugin.stringLocations.get(file).put(in.extractInt(4),
                            in.extractInt(4) + recordsSize);
                }
                break;
            }
        }

        if (in == null) {
            SPGlobal.logError(header, plugin.toString() + " did not have Strings files (loose or in BSA).");
        } else {
            SPGlobal.logMod(plugin, header, "Loaded " + file + " from language: " + plugin.language);
        }
    }

    static String getStringFilePath(Mod plugin, Language l, SubStringPointer.Files file) {
        return "Strings\\" + plugin.getName().substring(0, plugin.getName().indexOf(".es")) + "_" + l + "." + file;
    }

    static String scanToGRUPStart(LInChannel in, ArrayList<String> target) throws java.io.IOException {
        String type;
        String intro;
        int size;

        while (in.available() >= 12) {
            intro = Ln.arrayToString(in.getBytes(0, 4));
            if ("TES4".equals(intro)) {
                extractHeaderInfo(in);
            }
            size = Ln.arrayToInt(in.extractInts(4, 4));
            type = Ln.arrayToString(in.extractInts(0, 4));
            for (String t : target) {
                if (t.equals(type)) {
                    in.skip(-12); // Go to start of GRUP
                    return type;
                }
            }
            // else skip GRUP
            in.skip(size - 12);  // -12 for parts already read input
        }

        return "NULL";
    }

    static RecordShrinkArray extractGRUPData(LInChannel in) throws IOException {
        return new RecordShrinkArray(in, getGRUPsize(in));
    }

    static int getGRUPsize(LInChannel in) {
        int size = Ln.arrayToInt(in.extractInts(4, 4));
        in.skip(-8); // Back to start of GRUP
        return size;
    }

    static private String genStatus(ModListing mod) {
        return "Importing " + mod.print();
    }
}
